//////////////////////////////////////////////////////////////////
// A basic framework designed for a monitor wall using CGLX.
//
// This is the main file that sets up basic OpenGL defaults to
// make it easier for others to develop application code without
// dealing with these specifics.
//
// Authors: Robert C. Duvall
//          Todd Berreth
//          Duke University
// Date   : 2009
//////////////////////////////////////////////////////////////////
// Includes
//
#include <cstdio>            // for sprintf
#include <cstring>           // for strlen
#include <cstdlib>           // for exit
#include <sys/time.h>        // gettimeofday
#include <iostream>
#include "cglx.h"            // for CGLX or GLUT
using namespace std;
#include "scene.h"           // your code

//////////////////////////////////////////////////////////////////
// Globals
//
// Variables
//
string theProgramTitle;
GLint theWindowWidth = 960, theWindowHeight = 200;
GLint theWindowPositionX = 100, theWindowPositionY = 100;
GLboolean isAnimating = true;
GLboolean isFullScreen = false;
GLuint lastTime, currentTime;
Scene * theScene = new Scene();

// Constants
//
const GLuint ANIMATION_DELAY = 40; // milliseconds between rendering
const GLuint ONE_SECOND = 1000;    // milliseconds in a second
const GLfloat NEAR_DISTANCE = 0.1; // near plane distance
const GLfloat FAR_DISTANCE = 500;  // far plane distance
const GLfloat FOV_ANGLE = 45;      // angle of field of view


//////////////////////////////////////////////////////////////////
//  Utility functions
//
/*
 * Returns the current time in milliseconds.
 */
GLuint timeGetTime ()
{
    timeval time;
    gettimeofday(&time, NULL);
    return time.tv_sec * ONE_SECOND + time.tv_usec / ONE_SECOND;
}

/*
 * Computes frames per second and display in window's title bar
 */
void computeFPS ()
{
    static GLuint frameCount = 0;
    static GLuint lastFrameTime = 0;
    static char* title = new char[theProgramTitle.length() + 20];

    frameCount++;
    GLint currentFrameTime = timeGetTime();
    if (currentFrameTime - lastFrameTime > ONE_SECOND)
    {
        sprintf(title, "%s [ FPS: %d ]", theProgramTitle.c_str(),
                frameCount * ONE_SECOND / (currentFrameTime - lastFrameTime));
        lastFrameTime = currentTime;
        frameCount = 0;
#ifndef DEF_USE_CGLX
        glutSetWindowTitle(title);
#endif
    }
}

/*
 * Reset perspective matrix based on size of viewport.
 */
void setPerspective (GLenum mode, GLint x = 0, GLint y = 0)
{
    // get info about viewport (x, y, w, h)
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);

    // set camera to view viewport area
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    // check for selection
    if (mode == GL_SELECT)
    {
        // create 5x5 pixel picking region near cursor location
        gluPickMatrix(GLfloat(x), GLfloat(viewport[3] - y), 5.0, 5.0, viewport);
    }
    // view scene in perspective
    gluPerspective(FOV_ANGLE, GLdouble(viewport[2]) / GLdouble(viewport[3]),
                   NEAR_DISTANCE, FAR_DISTANCE);
//	glOrtho(-GLdouble(viewport[2])/2.0, GLdouble(viewport[2])/2.0, -GLdouble(viewport[3])/2.0, GLdouble(viewport[3])/2.0, NEAR_DISTANCE, FAR_DISTANCE);
    // prepare to work with model again
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

/*
 * Determine which objects have been selected by pressing the mouse
 */
void selectObject (GLint x, GLint y)
{
    // allocate enough space to store select info
    GLuint selectBuf[512];
    glSelectBuffer(512, selectBuf);

    // prepare for selection by initializing name info (0 represents a miss)
    glRenderMode(GL_SELECT);
    glInitNames();
    glPushName(0);

    // render to select buffer instead of color buffer
    glPushMatrix();
    {
        setPerspective(GL_SELECT, x, y);
        theScene->setCamera();
        theScene->display();
    }
    glPopMatrix();

    // if object hit, react
    GLuint numHits = glRenderMode(GL_RENDER);
    if (numHits > 0)
    {
        theScene->selectObject(numHits, selectBuf);
    }
    // reset camera for viewing
    setPerspective(GL_RENDER);
}

/*
 * Called when OpenGL detects an error while displaying scene.
 *
 * The last error is given as errorCode. Valid error codes are: 
 *  GL_INVALID_ENUM, GL_INVALID_VALUE, GL_INVALID_OPERATION,
 *  GL_STACK_OVERFLOW, GL_STACK_UNDERFLOW, and GL_OUT_OF_MEMORY.
 */
void reportError (GLenum errorCode)
{
    // BUGBUG: must be a better way to do this
    while (errorCode != GL_NO_ERROR)
    {
        cerr << "***ERROR***" << errorCode << endl;
        errorCode = glGetError();
    }
}

//////////////////////////////////////////////////////////////////
// Callback Functions
// These functions are registered with the glut window and called 
// when certain events occur.
//
/*
 * Inits OpenGL and scene (now that context has been correctly set).
 */
void onInit (int argc, char * argv[])
{
    // by default the back ground color is black
    glClearColor(0, 0, 0, 0.0);
    // set to draw in window based on depth 
    glEnable(GL_DEPTH_TEST);

    // get info about viewport (x, y, w, h)
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    // init scene with aspect ratio and command-line args
    theScene->init(GLfloat(viewport[2]) / GLfloat(viewport[3]), argc, argv);
}

/*
 * Displays contents of scene when window needs to be refreshed.
 */
void onDisplay ()
{
    // clears requested bits (color and depth) in glut window
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // draw entire scene into cleared window
    glPushMatrix();
    {
        theScene->setCamera();
        theScene->display();
    }
    glPopMatrix();

    // check for any errors when rendering
    GLenum errorCode = glGetError();
    if (errorCode == GL_NO_ERROR)
    {
        // double-buffering - swap back and front buffers
        glFlush();
        glutSwapBuffers();
    }
    else
    {
        reportError(errorCode);
    }
}

/*
 * Updates and re-renders scene based on users code, maintaining
 * constant framework if possible.
 */
void onIdle ()
{
    if (isAnimating)
    {
        currentTime = timeGetTime();
        GLuint elapsedTime = currentTime - lastTime;
        if (elapsedTime > ANIMATION_DELAY)
        {
            // animate the scene
            theScene->update(GLfloat(elapsedTime) / ONE_SECOND);
            // compute the frame rate
            lastTime = currentTime;
            computeFPS();
            // notify window it has to be repainted
            glutPostRedisplay();
        }
    }
}

/*
 * Allow scene to respond to key presses.
 */
void onAlphaNumericKeyPress (GLubyte key, GLint x, GLint y)
{
    switch (key)
    {
        // toggle animation running
        case 'p':
            isAnimating = ! isAnimating;
            break;

            // step animation to next frame
        case 's':
            theScene->update(GLfloat(ANIMATION_DELAY) / ONE_SECOND);
            break;

            // quit!
        case 'Q':
        case 'q':
        case 27: // ESC
            exit(0);

            // respond to other keys
        default:
            theScene->keyPressed(key, 0, x, y);
    }

    // notify window that it has to be re-rendered
    glutPostRedisplay();
}

/*
 * Allow scene to respond to special key presses (function and arrow keys).
 */
void onSpecialKeyPress (GLint key, GLint x, GLint y)
{
    switch (key)
    {
#ifndef DEF_USE_CGLX
        // toggle full screen mode
        case GLUT_KEY_F1:
            isFullScreen = ! isFullScreen;
            if (isFullScreen)
            {
                glutFullScreen();
            }
            else
            {
                glutReshapeWindow(theWindowWidth, theWindowHeight);
                glutPositionWindow(theWindowPositionX, theWindowPositionY);
            }
            break;
#endif

            // respond to other keys
        default:
            theScene->keyPressed(0, key, x, y);
    }

    // notify window that it has to be re-rendered
    glutPostRedisplay();
}

/*
 * Allow scene to respond to when mouse is moved.
 */
void onMouseMotion (GLint x, GLint y)
{
    // scene response
    theScene->mouseMotion(x, y);
    // notify window that it has to be re-rendered
    glutPostRedisplay();
}

/*
 * Allow scene to respond to when mouse is moved while button is pressed.
 */
void onMouseDrag (GLint x, GLint y)
{
    // scene response
    theScene->mouseDrag(x, y);
    // notify window that it has to be re-rendered
    glutPostRedisplay();
}

/*
 * Allow scene to respond to when mouse button is pressed.
 */
void onMouseButtonChanged (GLint button, GLint state, GLint x, GLint y)
{
    if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
    {
        selectObject(x, y);
    }

    // scene response
    theScene->mouseButtonChanged(button, state, x, y);
    // notify window that it has to be re-rendered
    glutPostRedisplay();
}

/*
 * Reset camera location and aspect to match window update.
 */
void onReshape (GLint width, GLint height)
{
    glViewport(0, 0, width, height);
    if (! isFullScreen)
    {
        theWindowWidth = width;
        theWindowHeight = height;
    }

    // set camera to view resized area
    setPerspective(GL_RENDER);
    // notify window that it has to be re-rendered
    glutPostRedisplay();
}

/*
 * Only update scene if window is visible, otherwise don't bother
 */
void onVisible (GLint state)
{
#ifndef DEF_USE_CGLX
    if (state == GLUT_VISIBLE)
    {
        // tell glut to animate model again
        glutIdleFunc(onIdle);
    }
    else
    {
        glutIdleFunc(NULL);
    }
#endif
}

//////////////////////////////////////////////////////////////////
// Main Function
//
int main (int argc, char *argv[])
{
    theProgramTitle = argv[0];

    // initialize glut
    glutInit(&argc, argv);
    // request initial window size and position on the screen
    glutInitWindowSize(theWindowWidth, theWindowHeight);
    glutInitWindowPosition(theWindowPositionX, theWindowPositionY);
    // request full color with double buffering and depth-based rendering
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGBA);
    // create window whose title is the name of the executable
    glutCreateWindow(theProgramTitle.c_str());

    // tell glut how to display model
    glutDisplayFunc(onDisplay);
    // tell glut how to respond to changes in window size
    glutReshapeFunc(onReshape);
    // cannot interact with CGLX windows for now
#ifndef DEF_USE_CGLX
    // tell glut how to handle changes in window visibility
    glutVisibilityFunc(onVisible);
    // tell glut how to handle key presses
    glutKeyboardFunc(onAlphaNumericKeyPress);
    glutSpecialFunc(onSpecialKeyPress);
    // tell glut how to handle the mouse
    glutMotionFunc(onMouseDrag);
    glutPassiveMotionFunc(onMouseMotion);
    glutMouseFunc(onMouseButtonChanged);
#else
    // tell glut what to do when it would otherwise be idle
    glutIdleFunc(onIdle);
#endif

    // initialize model
    onInit(argc, argv);
    // record start time
    lastTime = timeGetTime();
    // give control over to glut to handle rendering and interaction
    glutMainLoop();

    // program should never get here
    return 0;
}
